/*-------------------------------------------------------------------------
 *
 * combocid.c
 *	  组合命令 ID 支持例程
 *
 * 在 8.3 之前，HeapTupleHeaderData 有单独的 cmin 和 cmax 字段。为了减少头部大小，
 * 现在 cmin 和 cmax 在头部的同一个字段中叠加。这通常是有效的，因为你很少在同一
 * 事务中插入并删除一个元组，并且在原始事务结束后，我们不需要保持任何字段有效。
 * 当插入事务确实删除元组时，为了使其有效，我们创建一个“组合”命令 ID，并将
 * 其存储在元组头中，而不是 cmin 和 cmax。组合命令 ID 可以借助一个后端私有数组映射
 * 到真实的 cmin 和 cmax，该数组由本模块管理。
 *
 * 为了允许重用现有的组合 CIDs，我们还保持一个哈希表，将 cmin,cmax 对映射到组合 CIDs。
 * 这保持了大多数情况下数据结构的大小是合理的，因为每个事务使用的唯一对的数量往往
 * 是很小的。
 *
 * 通过 32 位组合命令 ID，我们可以表示 2^32 个不同的 cmin,cmax 组合。在最极端的情况下，
 * 每个命令都删除由每个之前命令生成的元组，对于 N 个命令所需的组合命令 ID 数量为 N*(N+1)/2。
 * 这意味着在最坏的情况下，能够支持 92682 个命令。在实际情况下，您将在达到该限制之前耗尽
 * 内存和/或磁盘空间。
 *
 * 数组和哈希表保存在 TopTransactionContext 中，并在每个事务结束时销毁。
 *
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 * IDENTIFICATION
 *	  src/backend/utils/time/combocid.c
 *
 *-------------------------------------------------------------------------
 */

#include "postgres.h"

#include "access/htup_details.h"
#include "access/xact.h"
#include "miscadmin.h"
#include "storage/shmem.h"
#include "utils/combocid.h"
#include "utils/hsearch.h"
#include "utils/memutils.h"

/* 哈希表通过 cmin 和 cmax 查找组合 CID */
static HTAB *comboHash = NULL;

/* 哈希表的键和值结构 */
typedef struct
{
	CommandId	cmin;
	CommandId	cmax;
} ComboCidKeyData;

typedef ComboCidKeyData *ComboCidKey;

typedef struct
{
	ComboCidKeyData key;
	CommandId	combocid;
} ComboCidEntryData;

typedef ComboCidEntryData *ComboCidEntry;

/* 哈希表的初始大小 */
#define CCID_HASH_SIZE			100


/*
 * 一个 cmin,cmax 对的数组，由组合命令 ID 索引。
 * 要将组合 CID 转换为 cmin 和 cmax，您只需进行简单的数组查找。
 */
static ComboCidKey comboCids = NULL;
static int	usedComboCids = 0;	/* comboCids 中的元素数量 */
static int	sizeComboCids = 0;	/* 数组的分配大小 */

/* 数组的初始大小 */
#define CCID_ARRAY_SIZE			100


/* 内部函数的原型 */
static CommandId fc_GetComboCommandId(CommandId fc_cmin, CommandId fc_cmax);
static CommandId fc_GetRealCmin(CommandId fc_combocid);
static CommandId fc_GetRealCmax(CommandId fc_combocid);


/**** 外部 API ****/

/*
 * GetCmin 和 GetCmax 确保它们只在合理的情况下调用，
 * 也就是说，它们能提供有用的答案。如果您有理由检查
 * 元组的 t_cid 字段而不是来自原始事务，直接使用 HeapTupleHeaderGetRawCommandId()。
 */

CommandId HeapTupleHeaderGetCmin(HeapTupleHeader fc_tup)
{
	CommandId	fc_cid = HeapTupleHeaderGetRawCommandId(fc_tup);

	Assert(!(fc_tup->t_infomask & HEAP_MOVED));
	Assert(TransactionIdIsCurrentTransactionId(HeapTupleHeaderGetXmin(fc_tup)));

	if (fc_tup->t_infomask & HEAP_COMBOCID)
		return fc_GetRealCmin(fc_cid);
	else
		return fc_cid;
}

CommandId HeapTupleHeaderGetCmax(HeapTupleHeader fc_tup)
{
	CommandId	fc_cid = HeapTupleHeaderGetRawCommandId(fc_tup);

	Assert(!(fc_tup->t_infomask & HEAP_MOVED));

	/*
	 * 因为 GetUpdateXid() 在 xmax 为 multixact 时会进行内存分配，
	 * 所以我们不能在关键区段内部进行 Assert()。这虽然
	 * 会削弱检查，但在关键区段内部不使用 GetCmax() 会使事情复杂化。
	 */
	Assert(CritSectionCount > 0 ||
		   TransactionIdIsCurrentTransactionId(HeapTupleHeaderGetUpdateXid(fc_tup)));

	if (fc_tup->t_infomask & HEAP_COMBOCID)
		return fc_GetRealCmax(fc_cid);
	else
		return fc_cid;
}

/*
 * 给定一个即将删除的元组，确定存入其 t_cid 字段的正确值。
 *
 * 如果我们不需要组合 CID，*cmax 将保持不变，*iscombo 被设置为
 * false。如果我们需要一个，*cmax 将被组合 CID 替换，*iscombo
 * 被设置为 true。
 *
 * 之所以与实际的 HeapTupleHeaderSetCmax() 操作分开，是因为这可能因为
 * 内存不足而失败。因此我们需要在进入实际更改共享缓冲区中的元组的关键区段之前这样做。
 */
void HeapTupleHeaderAdjustCmax(HeapTupleHeader fc_tup,
						  CommandId *fc_cmax,
						  bool *fc_iscombo)
{
	/*
	 * 如果我们标记一个由（我们事务的任何子事务）插入的元组为删除，
	 * 我们需要使用组合命令 ID。首先测试 HeapTupleHeaderXminCommitted()，
	 * 因为它比 TransactionIdIsCurrentTransactionId 调用便宜。
	 */
	if (!HeapTupleHeaderXminCommitted(fc_tup) &&
		TransactionIdIsCurrentTransactionId(HeapTupleHeaderGetRawXmin(fc_tup)))
	{
		CommandId	fc_cmin = HeapTupleHeaderGetCmin(fc_tup);

		*fc_cmax = fc_GetComboCommandId(fc_cmin, *fc_cmax);
		*fc_iscombo = true;
	}
	else
	{
		*fc_iscombo = false;
	}
}

/*
 * 组合命令 ID 仅对插入和删除事务感兴趣，因此我们可以在
 * 事务结束时忘记它们。
 */
void AtEOXact_ComboCid(void)
{
	/*
	 * 不用麻烦进行 pfree。这些是在 TopTransactionContext 中分配的，
	 * 所以它们会在事务结束时自动消失。
	 */
	comboHash = NULL;

	comboCids = NULL;
	usedComboCids = 0;
	sizeComboCids = 0;
}


/**** 内部例程 ****/

/*
 * 获取映射到 cmin 和 cmax 的组合命令 ID。
 *
 * 我们尽可能重用旧的组合命令 ID。
 */
static CommandId fc_GetComboCommandId(CommandId fc_cmin, CommandId fc_cmax)
{
	CommandId	fc_combocid;
	ComboCidKeyData fc_key;
	ComboCidEntry fc_entry;
	bool		fc_found;

	/*
	 * 在第一次需要在事务中使用组合 CID 时创建哈希表和数组。
	 */
	if (comboHash == NULL)
	{
		HASHCTL		fc_hash_ctl;

		/* 先创建数组；哈希表的存在表明数组存在 */
		comboCids = (ComboCidKeyData *)
			MemoryContextAlloc(TopTransactionContext,
							   sizeof(ComboCidKeyData) * CCID_ARRAY_SIZE);
		sizeComboCids = CCID_ARRAY_SIZE;
		usedComboCids = 0;

		fc_hash_ctl.keysize = sizeof(ComboCidKeyData);
		fc_hash_ctl.entrysize = sizeof(ComboCidEntryData);
		fc_hash_ctl.hcxt = TopTransactionContext;

		comboHash = hash_create("Combo CIDs",
								CCID_HASH_SIZE,
								&fc_hash_ctl,
								HASH_ELEM | HASH_BLOBS | HASH_CONTEXT);
	}

	/*
	 * 如果没有至少一个空闲槽，扩展数组。我们必须在可能进入新的哈希表条目之前执行此操作，否则，重新分配失败将留下一个损坏的哈希表条目。
	 */
	if (usedComboCids >= sizeComboCids)
	{
		int			fc_newsize = sizeComboCids * 2;

		comboCids = (ComboCidKeyData *)
			repalloc(comboCids, sizeof(ComboCidKeyData) * fc_newsize);
		sizeComboCids = fc_newsize;
	}

	/* 查找或创建一个具有所需 cmin/cmax 的哈希条目 */

	/* 我们假设 ComboCidKeyData 中没有结构填充！ */
	fc_key.cmin = fc_cmin;
	fc_key.cmax = fc_cmax;
	fc_entry = (ComboCidEntry) hash_search(comboHash,
										(void *) &fc_key,
										HASH_ENTER,
										&fc_found);

	if (fc_found)
	{
		/* 重用现有的组合 CID */
		return fc_entry->combocid;
	}

	/* 我们必须创建一个新的组合 CID；我们已经在数组中留出了空间 */
	fc_combocid = usedComboCids;

	comboCids[fc_combocid].cmin = fc_cmin;
	comboCids[fc_combocid].cmax = fc_cmax;
	usedComboCids++;

	fc_entry->combocid = fc_combocid;

	return fc_combocid;
}

static CommandId fc_GetRealCmin(CommandId fc_combocid)
{
	Assert(fc_combocid < usedComboCids);
	return comboCids[fc_combocid].cmin;
}

static CommandId fc_GetRealCmax(CommandId fc_combocid)
{
	Assert(fc_combocid < usedComboCids);
	return comboCids[fc_combocid].cmax;
}

/*
 * 估计序列化当前组合 CID 状态所需的空间量。
 */
Size EstimateComboCIDStateSpace(void)
{
	Size		fc_size;

	/* 添加保存 usedComboCids 所需的空间 */
	fc_size = sizeof(int);

	/* 添加保存 ComboCidKeyData 所需的空间 */
	fc_size = add_size(fc_size, mul_size(sizeof(ComboCidKeyData), usedComboCids));

	return fc_size;
}

/*
 * 将组合 CID 状态序列化到内存中，从 start_address 开始。
 * maxsize 应至少与 EstimateComboCIDStateSpace 返回的值一样大。
 */
void SerializeComboCIDState(Size fc_maxsize, char *fc_start_address)
{
	char	   *fc_endptr;

	/* 首先，我们存储当前存在的组合 CID 的数量。 */
	*(int *) fc_start_address = usedComboCids;

	/* 如果 maxsize 太小，则抛出错误。 */
	fc_endptr = fc_start_address + sizeof(int) +
		(sizeof(ComboCidKeyData) * usedComboCids);
	if (fc_endptr < fc_start_address || fc_endptr > fc_start_address + fc_maxsize)
		elog(ERROR, "not enough space to serialize ComboCID state");

	/* 现在，复制实际的 cmin/cmax 对。 */
	if (usedComboCids > 0)
		memcpy(fc_start_address + sizeof(int), comboCids,
			   (sizeof(ComboCidKeyData) * usedComboCids));
}

/*
 * 在指定地址读取组合 CID 状态，并用相同的组合 CID 初始化此后端。
 * 这仅在当前没有组合 CID 的后端中有效（并且只有在事务状态也序列化并恢复时才有意义）。
 */
void RestoreComboCIDState(char *fc_comboCIDstate)
{
	int			fc_num_elements;
	ComboCidKeyData *fc_keydata;
	int			fc_i;
	CommandId	fc_cid;

	Assert(!comboCids && !comboHash);

	/* 首先，我们检索被序列化的组合 CID 数量。 */
	fc_num_elements = *(int *) fc_comboCIDstate;
	fc_keydata = (ComboCidKeyData *) (fc_comboCIDstate + sizeof(int));

	/* 使用 GetComboCommandId 恢复每个组合 CID。 */
	for (fc_i = 0; fc_i < fc_num_elements; fc_i++)
	{
		fc_cid = fc_GetComboCommandId(fc_keydata[fc_i].cmin, fc_keydata[fc_i].cmax);

		/* 验证我们得到了预期的答案。 */
		if (fc_cid != fc_i)
			elog(ERROR, "unexpected command ID while restoring combo CIDs");
	}
}
